﻿using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Diagnostics;

namespace agTweener
{
    /// <summary>
    /// TweenListItem
    /// Represents an object being tweened 
    /// 
    /// author		Michael Cameron
    /// version		1.0.1
    /// Updated for Silverlight 2.0 Beta 1
    /// </summary>
    public class TweenListItem
    {
        private TweenParameter start;
        private TweenParameter end;
        private TweenParameter current;

        private TimeSpan timeframe;
        private TimeSpan delay;
        private TimeSpan currentInterval;
        private TimeSpan currentDelay;
        private bool _started = false;
        private TransitionType _transition;
        private FrameworkElement _feTarget;
        private delegate double TransitionFunction(params double[] args);
        private TransitionFunction func;
        internal event EventHandler Completed;
        private TweenEvent _onStart;
        private object[] _onStartParams;
        private TweenEvent _onUpdate;
        private object[] _onUpdateParams;
        private TweenEvent _onComplete;
        private object[] _onCompleteParams;

        internal TweenEvent OnStart
        {
            get { return _onStart; }
        }

        internal object[] OnStartParams
        {
            get { return _onStartParams; }
        }

        internal TweenEvent OnUpdate
        {
            get { return _onUpdate; }
        }

        internal object[] OnUpdateParams
        {
            get { return _onUpdateParams; }
        }

        internal TweenEvent OnComplete
        {
            get { return _onComplete; }
        }

        internal object[] OnCompleteParams
        {
            get { return _onCompleteParams; }
        }

        /// <summary>
        /// Constructor 
        /// </summary>
        /// <param name="target"></param>
        /// <param name="p"></param>
        /// <param name="onStart"></param>
        /// <param name="onStartParams"></param>
        /// <param name="onUpdate"></param>
        /// <param name="onUpdateParams"></param>
        /// <param name="onComplete"></param>
        /// <param name="onCompleteParams"></param>
        internal TweenListItem(FrameworkElement target, TweenParameter p, TweenEvent onStart, object[] onStartParams, TweenEvent onUpdate, object[] onUpdateParams, TweenEvent onComplete, object[] onCompleteParams)
        {
            _feTarget = target;
            InitListItem(p, onStart, onStartParams, onUpdate, onUpdateParams, onComplete, onCompleteParams);

            // copy starting values of target into the start variable and current variable (need to be separate instances)
            start = new TweenParameter();
            start.Init(_feTarget);
            current = new TweenParameter();
            current.Init(_feTarget);
            end = p;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="DoubleArrayStart"></param>
        /// <param name="p"></param>
        /// <param name="onStart"></param>
        /// <param name="onStartParams"></param>
        /// <param name="onUpdate"></param>
        /// <param name="onUpdateParams"></param>
        /// <param name="onComplete"></param>
        /// <param name="onCompleteParams"></param>
        internal TweenListItem(double[] DoubleArrayStart, TweenParameter p, TweenEvent onStart, object[] onStartParams, TweenEvent onUpdate, object[] onUpdateParams, TweenEvent onComplete, object[] onCompleteParams)
        {
            InitListItem(p, onStart, onStartParams, onUpdate, onUpdateParams, onComplete, onCompleteParams);

            // copy starting values of target into the start variable and current variable (need to be separate instances)
            start = new TweenParameter();

            start.DoubleArray = (double[])DoubleArrayStart.Clone();
            current = new TweenParameter();
            current.DoubleArray = (double[])DoubleArrayStart.Clone();
            end = p;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="pointStart"></param>
        /// <param name="p"></param>
        /// <param name="onStart"></param>
        /// <param name="onStartParams"></param>
        /// <param name="onUpdate"></param>
        /// <param name="onUpdateParams"></param>
        /// <param name="onComplete"></param>
        /// <param name="onCompleteParams"></param>
        internal TweenListItem(Point pointStart, TweenParameter p, TweenEvent onStart, object[] onStartParams, TweenEvent onUpdate, object[] onUpdateParams, TweenEvent onComplete, object[] onCompleteParams)
        {
            InitListItem(p, onStart, onStartParams, onUpdate, onUpdateParams, onComplete, onCompleteParams);

            // copy starting values of target into the start variable and current variable (need to be separate instances)
            start = new TweenParameter();
            start.Point = new Point(pointStart.X, pointStart.Y);
            current = new TweenParameter();
            current.Point = new Point(pointStart.X, pointStart.Y);
            end = p;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="p"></param>
        /// <param name="onStart"></param>
        /// <param name="onStartParams"></param>
        /// <param name="onUpdate"></param>
        /// <param name="onUpdateParams"></param>
        /// <param name="onComplete"></param>
        /// <param name="onCompleteParams"></param>
        private void InitListItem(TweenParameter p, TweenEvent onStart, object[] onStartParams, TweenEvent onUpdate, object[] onUpdateParams, TweenEvent onComplete, object[] onCompleteParams)
        {
            _onStart = onStart;
            _onStartParams = onStartParams;
            _onUpdate = onUpdate;
            _onUpdateParams = onUpdateParams;
            _onComplete = onComplete;
            _onCompleteParams = onCompleteParams;
            // Convert the time in seconds to a timespan
            int minutes = (int)Math.Floor(p.time / 60);
            int seconds = (int)Math.Floor(p.time - minutes * 60);
            int milliseconds = (int)((p.time - Math.Floor(p.time)) * 1000);
            timeframe = new TimeSpan(0, 0, minutes, seconds, milliseconds);
            // Convert the delay in seconds to a timespan
            if (p.delay > 0)
            {
                minutes = (int)Math.Floor(p.delay / 60);
                seconds = (int)Math.Floor(p.delay - minutes * 60);
                milliseconds = (int)((p.delay - Math.Floor(p.delay)) * 1000);
                delay = new TimeSpan(0, 0, minutes, seconds, milliseconds);
            }
            _transition = p.transition;

            func = (TransitionFunction)Delegate.CreateDelegate(typeof(TransitionFunction), typeof(Equations).GetMethod(p.transition.ToString()));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="tickInterval"></param>
        public void HandleTick(TimeSpan tickInterval)
        {
            //Increment the delay counter and check against preset delay
            currentDelay += tickInterval;
            if (currentDelay < delay) return;

            if (!_started)
            {
                if (_onStart != null)
                    _onStart(_onStartParams);
                _started = true;
            }
            //Increment the interval counter - if completed then notify this can be destroyed and return
            currentInterval += tickInterval;
            //Save the current interval back in the current tween parameter for passing back to calling class
            current.time = currentInterval.TotalSeconds;
            if (currentInterval > timeframe)
            {
                OnCompleted(EventArgs.Empty);
                return;
            }

            double t = (double)currentInterval.TotalMilliseconds / 1000.0;
            double d = (double)timeframe.TotalMilliseconds / 1000.0;

            if (end.LeftProperty > Double.MinValue)
            {
                current.LeftProperty = func(t, start.LeftProperty, end.LeftProperty - start.LeftProperty, d);
                _feTarget.SetValue(Canvas.LeftProperty, (double)current.LeftProperty);
            }
            if (end.TopProperty > Double.MinValue)
            {
                current.TopProperty = func(t, start.TopProperty, end.TopProperty - start.TopProperty, d);
                _feTarget.SetValue(Canvas.TopProperty, (double)current.TopProperty);
            }
            if (end.Width > Double.MinValue)
            {
                current.Width = func(t, start.Width, end.Width - start.Width, d);
                _feTarget.Width = (double)current.Width;
            }
            if (end.Height > Double.MinValue)
            {
                current.Height = func(t, start.Height, end.Height - start.Height, d);
                _feTarget.Height = (double)current.Height;
            }
            if (end.X > Double.MinValue)
            {
                current.X = func(t, start.X, end.X - start.X, d);
                SetTargetTranslate(_feTarget, "X", current.X);
            }
            if (end.Y > Double.MinValue)
            {
                current.Y = func(t, start.Y, end.Y - start.Y, d);
                SetTargetTranslate(_feTarget, "Y", current.Y);
            }
            if (end.ScaleX > Double.MinValue)
            {
                current.ScaleX = func(t, start.ScaleX, end.ScaleX - start.ScaleX, d);
                SetTargetScale(_feTarget, "ScaleX", current.ScaleX);
            }
            if (end.ScaleY > Double.MinValue)
            {
                current.ScaleY = func(t, start.ScaleY, end.ScaleY - start.ScaleY, d);
                SetTargetScale(_feTarget, "ScaleY", current.ScaleY);
            }
            if (end.SkewAngleX > Double.MinValue)
            {
                current.SkewAngleX = func(t, start.SkewAngleX, end.SkewAngleX - start.SkewAngleX, d);
                SetTargetSkew(_feTarget, "AngleX", current.SkewAngleX);
            }
            if (end.SkewAngleY > Double.MinValue)
            {
                current.SkewAngleY = func(t, start.SkewAngleY, end.SkewAngleY - start.SkewAngleY, d);
                SetTargetSkew(_feTarget, "AngleY", current.SkewAngleX);
            }
            if (end.RotateAngle > Double.MinValue)
            {
                current.RotateAngle = func(t, start.RotateAngle, end.RotateAngle - start.RotateAngle, d);
                SetTargetRotate(_feTarget, "RotateAngle", current.RotateAngle);
            }
            if (end.Opacity > Double.MinValue)
            {
                current.Opacity = func(t, start.Opacity, end.Opacity - start.Opacity, d);
                _feTarget.Opacity = current.Opacity;
            }
            if (end.ZIndex > int.MinValue)
            {
                current.ZIndex = (int)func(t, start.ZIndex, end.ZIndex - start.ZIndex, d);
                SetTargetZIndex(_feTarget, current.ZIndex);
            }
            if (end.DoubleArray != null && end.DoubleArray.Length > 0)
            {
                // Current DoubleArray values can only be notified back to the calling class through the OnUpdate event through
                // the OnUpdateParams object. Transfer the values into this object
                _onUpdateParams = new object[end.DoubleArray.Length];
                for (int i = 0; i < end.DoubleArray.Length; i++) {
                    current.DoubleArray[i] = func(t, start.DoubleArray[i], end.DoubleArray[i] - start.DoubleArray[i], d);
                    _onUpdateParams[i] = current.DoubleArray[i];
                }
            }
            if (end.DoubleValue > Double.MinValue)
            {
                // Current DoubleValue value can only be notified back to the calling class through the OnUpdate event through
                // the OnUpdateParams object. Transfer the value into this object
                _onUpdateParams = new object[1];
                current.DoubleValue = func(t, start.DoubleValue, end.DoubleValue - start.DoubleValue, d);
                _onUpdateParams[0] = current.DoubleValue;
            }
            if (end.Point.X > Double.MinValue)
            {
                // Current Point value can only be notified back to the calling class through the OnUpdate event through
                // the OnUpdateParams object. Transfer the value into this object
                _onUpdateParams = new object[1];
                current.Point.X = func(t, start.Point.X, end.Point.X - start.Point.X, d);
                current.Point.Y = func(t, start.Point.Y, end.Point.Y - start.Point.Y, d);
                _onUpdateParams[0] = current.Point;
            }
            if (_onUpdate != null)
            {
                // set default onUpdateParams equal to current tweenparameter values if none passed in
                if (_onUpdateParams == null)
                {
                    _onUpdateParams = new object[1];
                    _onUpdateParams[0] = current;
                }
                _onUpdate(_onUpdateParams);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="e"></param>
        protected virtual void OnCompleted(EventArgs e)
        {
            if (Completed != null)
                Completed(this, e);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="target"></param>
        /// <param name="property"></param>
        /// <param name="value"></param>
        private void SetTargetTranslate(FrameworkElement target, string property, double value)
        {
            TranslateTransform st = (TranslateTransform)current.FindTransform(target, "TranslateTransform");
            switch (property)
            {
                case "X":
                    st.X = value;
                    break;
                case "Y":
                    st.Y = value;
                    break;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="target"></param>
        /// <param name="property"></param>
        /// <param name="value"></param>
        private void SetTargetScale(FrameworkElement target, string property, double value)
        {
            ScaleTransform st = (ScaleTransform)current.FindTransform(target, "ScaleTransform");
            switch (property)
            {
                case "ScaleX":
                    st.ScaleX = value;
                    break;
                case "ScaleY":
                    st.ScaleY = value;
                    break;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="target"></param>
        /// <param name="property"></param>
        /// <param name="value"></param>
        private void SetTargetSkew(FrameworkElement target, string property, double value)
        {
            SkewTransform st = (SkewTransform)current.FindTransform(target, "SkewTransform");
            switch (property)
            {
                case "AngleX":
                    st.AngleX = value;
                    break;
                case "AngleY":
                    st.AngleY = value;
                    break;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="target"></param>
        /// <param name="property"></param>
        /// <param name="value"></param>
        private void SetTargetRotate(FrameworkElement target, string property, double value)
        {
            RotateTransform st = (RotateTransform)current.FindTransform(target, "RotateTransform");
            st.Angle = value;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="target"></param>
        /// <param name="value"></param>
        private void SetTargetZIndex(FrameworkElement target, int value)
        {
            target.SetValue(Canvas.ZIndexProperty, value);
        }

    }
}
